# -*- coding: utf-8 -*-
"""
A TestRunner for use with the Python unit testing framework. It
generates a HTML report to show the result at a glance.

The simplest way to use this is to invoke its main method. E.g.

    import unittest
    import HTMLTestRunner

    ... define your tests ...

    if __name__ == '__main__':
        HTMLTestRunner.main()


For more customization options, instantiates a HTMLTestRunner object.
HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g.

    # output to a file
    fp = file('my_report.html', 'wb')
    runner = HTMLTestRunner.HTMLTestRunner(
                stream=fp,
                title='My unit test',
                description='This demonstrates the report output by HTMLTestRunner.'
                )

    # Use an external stylesheet.
    # See the TemplateMixin class for more customizable options
    runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">'

    # run the test
    runner.run(my_test_suite)


------------------------------------------------------------------------
Copyright (c) 2004-2007, Wai Yip Tung
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

* Redistributions of source code must retain the above copyright notice,
  this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
  notice, this list of conditions and the following disclaimer in the
  documentation and/or other materials provided with the distribution.
* Neither the name Wai Yip Tung nor the names of its contributors may be
  used to endorse or promote products derived from this software without
  specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""

import datetime
import sys
import io
import time
import unittest
from xml.sax import saxutils
import os
import shutil
# from PIL import Image
import random
from typing import Tuple, Set

from src.public import pg
from src.public.screenshot import screenshot
from src.config.config import Config
from src.public.public import ParseXml

# URL: http://tungwaiyip.info/software/HTMLTestRunner.html

__author__ = "Wai Yip Tung"
__version__ = "0.9.1"

"""
Change History
Version 0.9.1
* 用Echarts添加执行情况统计图 (灰蓝)

Version 0.9.0
* 改成Python 3.x (灰蓝)

Version 0.8.3
* 使用 Bootstrap稍加美化 (灰蓝)
* 改为中文 (灰蓝)

Version 0.8.2
* Show output inline instead of popup window (Viorel Lupu).

Version in 0.8.1
* Validated XHTML (Wolfgang Borgert).
* Added description of test classes and test cases.

Version in 0.8.0
* Define TemplateMixin class for customization.
* Workaround a IE 6 bug that it does not treat <script> block as CDATA.

Version in 0.7.1
* Back port to Python 2.3 (Frank Horowitz).
* Fix missing scroll bars in detail log (Podi).
"""


# TODO: color stderr
# TODO: simplify javascript using ,ore than 1 class in the class attribute?

# ------------------------------------------------------------------------
# The redirectors below are used to capture output during testing. Output
# sent to sys.stdout and sys.stderr are automatically captured. However
# in some cases sys.stdout is already cached before HTMLTestRunner is
# invoked (e.g. calling logging.basicConfig). In order to capture those
# output, use the redirectors for the cached stream.
#
# e.g.
#   >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector)
#   >>>


class OutputRedirector(object):
	""" Wrapper to redirect stdout or stderr """

	def __init__(self, fp):
		self.fp = fp

	def write(self, s):
		self.fp.write(s)

	def writelines(self, lines):
		self.fp.writelines(lines)

	def flush(self):
		self.fp.flush()


stdout_redirector = OutputRedirector(sys.stdout)
stderr_redirector = OutputRedirector(sys.stderr)


# ----------------------------------------------------------------------
# Template


class TemplateMixin(object):
	"""
	Define a HTML template for report customerization and generation.

	Overall structure of an HTML report

	HTML
	+------------------------+
	|<html>                  |
	|  <head>                |
	|                        |
	|   STYLESHEET           |
	|   +----------------+   |
	|   |                |   |
	|   +----------------+   |
	|                        |
	|  </head>               |
	|                        |
	|  <body>                |
	|                        |
	|   HEADING              |
	|   +----------------+   |
	|   |                |   |
	|   +----------------+   |
	|                        |
	|   REPORT               |
	|   +----------------+   |
	|   |                |   |
	|   +----------------+   |
	|                        |
	|   ENDING               |
	|   +----------------+   |
	|   |                |   |
	|   +----------------+   |
	|                        |
	|  </body>               |
	|</html>                 |
	+------------------------+
	"""

	STATUS = {0: u"通过", 1: u"失败", 2: u"错误"}

	DEFAULT_TITLE = "Unit Test Report"
	DEFAULT_DESCRIPTION = ""

	# ------------------------------------------------------------------------
	# HTML Template
	# 2018年10月10日 zz：连接外部样式表由访问cdn.bootcss.com地址改为本地.css与.js资源文件
	HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>%(title)s</title>
    <meta name="generator" content="%(generator)s"/>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    
    <link rel="stylesheet" type="text/css" href="bootstrap.min.css">
    <script type="text/javascript" src="echarts.common.min.js"></script>
    <!--link href="http://cdn.bootcss.com/bootstrap/3.3.0/css/bootstrap.min.css" rel="stylesheet"-->
    <!--script src="https://cdn.bootcss.com/echarts/3.8.5/echarts.common.min.js"></script-->
    <!-- <script type="text/javascript" src="js/echarts.common.min.js"></script> -->
    
    %(stylesheet)s
    
</head>
<body>
    <script language="javascript" type="text/javascript"><!--
    output_list = Array();

    /* level - 0:Summary; 1:Failed; 2:All */
    function showCase(level) {
        trs = document.getElementsByTagName("tr");
        for (var i = 0; i < trs.length; i++) {
            tr = trs[i];
            id = tr.id;
            if (id.substr(0,2) == 'ft') {
                if (level < 1) {
                    tr.className = 'hiddenRow';
                }
                else {
                    tr.className = '';
                }
            }
            if (id.substr(0,2) == 'pt') {
                if (level > 1) {
                    tr.className = '';
                }
                else {
                    tr.className = 'hiddenRow';
                }
            }
        }
    }


    function showClassDetail(cid, count) {
        var id_list = Array(count);
        var toHide = 1;
        for (var i = 0; i < count; i++) {
            tid0 = 't' + cid.substr(1) + '.' + (i+1);
            tid = 'f' + tid0;
            tr = document.getElementById(tid);
            if (!tr) {
                tid = 'p' + tid0;
                tr = document.getElementById(tid);
            }
            id_list[i] = tid;
            if (tr.className) {
                toHide = 0;
            }
        }
        for (var i = 0; i < count; i++) {
            tid = id_list[i];
            if (toHide) {
                //document.getElementById('div_'+tid).style.display = 'none'
                // 2018年7月7日 修复点击详细无法收起的bug
                e_div_tid = document.getElementById('div_'+tid);
                if (e_div_tid) {
                    e_div_tid.style.display = 'none'
                }
                document.getElementById(tid).className = 'hiddenRow';
            }
            else {
                document.getElementById(tid).className = '';
            }
        }
    }


    function showTestDetail(div_id){
        var details_div = document.getElementById(div_id)
        var displayState = details_div.style.display
        // alert(displayState)
        if (displayState != 'block' ) {
            displayState = 'block'
            details_div.style.display = 'block'
        }
        else {
            details_div.style.display = 'none'
        }
    }


    function html_escape(s) {
        s = s.replace(/&/g,'&amp;');
        s = s.replace(/</g,'&lt;');
        s = s.replace(/>/g,'&gt;');
        return s;
    }

    function chart_display() {
        chart = document.getElementById('chart');
        display_button = document.getElementById('display_button');
        description = document.getElementsByClassName("description_div")[0];
        if (chart.style.display == 'none') {
            chart.style.display = '';
            description.style.width = '50%%';
            display_button.innerText="隐藏";
        }
        else {
            chart.style.display = 'none';
            description.style.width = '100%%';
            display_button.innerText = '图表';
        }
    }

    function changeimg(pic) {
        if (pic.width == '100') {
            var img = new Image();
            img.src = pic.src;
            if (img.width/2 < 100 || img.height/2 < 100){
                pic.width = img.width;
                pic.height = img.height;
            }
            else {
                pic.width = img.width/2;
                pic.height = img.height/2;
            }
        }
        else {
            pic.width = '100';
            pic.height = '100';
        }
    }

    /* obsoleted by detail in <div>
    function showOutput(id, name) {
        var w = window.open("", //url
                        name,
                        "resizable,scrollbars,status,width=800,height=450");
        d = w.document;
        d.write("<pre>");
        d.write(html_escape(output_list[id]));
        d.write("\n");
        d.write("<a href='javascript:window.close()'>close</a>\n");
        d.write("</pre>\n");
        d.close();
    }
    */
    --></script>

    <div id="div_base">
        %(heading)s
        %(report)s
        %(ending)s
        %(chart_script)s
    </div>
</body>
</html>
"""  # variables: (title, generator, stylesheet, heading, report, ending, chart_script)

	ECHARTS_SCRIPT = """
    <script type="text/javascript">
        // 基于准备好的dom，初始化echarts实例
        var myChart = echarts.init(document.getElementById('chart'));

        // 指定图表的配置项和数据
        var option = {
            title : {
                text: '测试执行情况',
                x:'center'
            },
            tooltip : {
                trigger: 'item',
                formatter: "{a} <br/>{b} : {c} ({d}%%)"
            },
            color: ['#95b75d', '#d46664', '#808080'],
            legend: {
                orient: 'vertical',
                left: 'left',
                data: ['通过','失败','错误']
            },
            series : [
                {
                    name: '测试执行情况',
                    type: 'pie',
                    radius : '60%%',
                    center: ['50%%', '60%%'],
                    data:[
                        {value:%(Pass)s, name:'通过'},
                        {value:%(fail)s, name:'失败'},
                        {value:%(error)s, name:'错误'}
                    ],
                    itemStyle: {
                        emphasis: {
                            shadowBlur: 10,
                            shadowOffsetX: 0,
                            shadowColor: 'rgba(0, 0, 0, 0.5)'
                        }
                    }
                }
            ]
        };

        // 使用刚指定的配置项和数据显示图表。
        myChart.setOption(option);
    </script>
    """  # variables: (Pass, fail, error)

	# ------------------------------------------------------------------------
	# Stylesheet
	#
	# alternatively use a <link> for external style sheet, e.g.
	#   <link rel="stylesheet" href="$url" type="text/css">

	STYLESHEET_TMPL = """
<style type="text/css" media="screen">
    body        { font-family: Microsoft YaHei,Consolas,arial,sans-serif; font-size: 80%; }
    table       { font-size: 100%; }
    pre         { white-space: pre-wrap;word-wrap: break-word; }

    /* -- heading ---------------------------------------------------------------------- */
    h1 {
        font-size: 16pt;
        color: gray;
    }
    /* 2018年7月14日 将<p> <td>字体调大 */
    p {
    font-size: 8pt;
    }
    td {
        font-size: 10pt;
    }
    .heading {
        margin-top: 0ex;
        margin-bottom: 1ex;
    }

    .heading .attribute {
        margin-top: 1ex;
        margin-bottom: 0;
    }

    .heading .description {
        margin-top: 2ex;
        margin-bottom: 3ex;
    }

    /* -- css div popup ------------------------------------------------------------------------ */
    a.popup_link {
    }

    a.popup_link:hover {
        color: red;
    }

    .popup_window {
        display: none;
        position: relative;
        left: 0px;
        top: 0px;
        /*border: solid #627173 1px; */
        padding: 10px;
        /*background-color: #E6E6D6; */
        font-family: "Lucida Console", "Courier New", Courier, monospace;
        text-align: left;
        font-size: 8pt;
        /* width: 500px;*/
    }

    }
    /* -- report ------------------------------------------------------------------------ */
    #show_detail_line {
        margin-top: 3ex;
        margin-bottom: 1ex;
    }
    #result_table {
        width: 99%;
    }
    #header_row {
        font-weight: bold;
        color: #303641;
        background-color: #ebebeb;
    }
    #total_row  { font-weight: bold; }
    .passClass  { background-color: #bdedbc; }
    .failClass  { background-color: #ffefa4; }
    .errorClass { background-color: #ffc9c9; }
    .passCase   { color: #6c6; }
    .failCase   { color: #FF6600; font-weight: bold; }
    .errorCase  { color: #c00; font-weight: bold; }
    .hiddenRow  { display: none; }
    .testcase   { margin-left: 2em; }


    /* -- ending ---------------------------------------------------------------------- */
    #ending {
    }

    #div_base {
                position:absolute;
                top:0%;
                left:5%;
                right:5%;
                width: auto;
                height: auto;
                margin: -15px 0 0 0;
    }
</style>
"""

	# ------------------------------------------------------------------------
	# Heading
	#

	HEADING_TMPL = """
    <div class='page-header'>
        <h1>%(title)s</h1>
    %(parameters)s
    </div>
    <div class="description_div" style="float:left;width:50%%;"><p class='description'>%(description)s</p></div>
    <div id="chart" style="width:50%%;height:200px;float:left;display:""></div>
    <button id="display_button" class="btn btn-default" onclick='javascript:chart_display()' style="float:right;width:5%%;">隐藏</button>
"""  # variables: (title, parameters, description)

	HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s:</strong> %(value)s</p>
"""  # variables: (name, value)

	# ------------------------------------------------------------------------
	# Report
	#

	REPORT_TMPL = u"""
    <div class="btn-group btn-group-sm">
        <button class="btn btn-default" onclick='javascript:showCase(0)'>简要</button>
        <button class="btn btn-default" onclick='javascript:showCase(1)'>失败</button>
        <button class="btn btn-default" onclick='javascript:showCase(2)'>全部</button>
    </div>
    <p></p>
    <table id='result_table' class="table table-bordered">
        <colgroup>
            <col align='left' />
            <col align='right' />
            <col align='right' />
            <col align='right' />
            <col align='right' />
            <col align='right' />
        </colgroup>
        <tr id='header_row'>
            <td>测试套件/测试用例</td>
            <td>总数</td>
            <td>通过</td>
            <td>失败</td>
            <td>错误</td>
            <td>查看</td>
            <td>耗时</td>
            <td>责任人</td>
        </tr>
        %(test_list)s
        <tr id='total_row'>
            <td>总计</td>
            <td>%(count)s</td>
            <td style="background-color: #95b75d">%(Pass)s</td>
            <td style="background-color: #d46664">%(fail)s</td>
            <td style="background-color: #808080">%(error)s</td>
            <td></td>
            <td>%(elapsed_time).2f</td>
            <td>&nbsp;</td>
        </tr>
    </table>
"""  # variables: (test_list, count, Pass, fail, error)

	REPORT_CLASS_TMPL = u"""
    <tr class='%(style)s'>
        <td>%(desc)s</td>
        <td>%(count)s</td>
        <td>%(Pass)s</td>
        <td>%(fail)s</td>
        <td>%(error)s</td>
        <td><a href="javascript:showClassDetail('%(cid)s',%(count)s)">详情</a></td>
        <td>%(elapsed_time).2f</td>
        <td>%(responsible_person)s</td>
    </tr>
"""  # variables: (style, desc, count, Pass, fail, error, cid)

	REPORT_TEST_WITH_OUTPUT_TMPL = r"""
<tr id='%(tid)s' class='%(Class)s'>
    <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
    <td colspan='5' align='center'>
    
    <!--css div popup start-->
    <a class="popup_link" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" >
        %(status)s</a>

    <div id='div_%(tid)s' class="popup_window">
        <pre>%(script)s</pre>
    </div>
    <!--css div popup end-->

    </td>
    <td>%(elapsed_time).2f</td>
    <td>&nbsp;</td>
</tr>
"""  # variables: (tid, Class, style, desc, status)

	REPORT_TEST_NO_OUTPUT_TMPL = r"""
<tr id='%(tid)s' class='%(Class)s'>
    <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
    <td colspan='5' align='center'>%(status)s</td>
    <td>%(elapsed_time).2f</td>
    <td>&nbsp;</td>
</tr>
"""  # variables: (tid, Class, style, desc, status)

	REPORT_TEST_OUTPUT_TMPL = r"""%(id)s: %(output)s"""  # variables: (id, output)

	# ------------------------------------------------------------------------
	# ENDING
	#

	ENDING_TMPL = """<div id='ending'>&nbsp;</div>"""

	IMG_TMPL = """<td>%(img)s</td>"""


# -------------------- The end of the Template class -------------------


TestResult = unittest.TestResult


class _TestResult(TestResult):
	# note: _TestResult is a pure representation of results.
	# It lacks the output and reporting ability compares to unittest._TextTestResult.

	def __init__(self,
				 verbosity: int = 1,
				 responsible_person: Tuple = ()):
		TestResult.__init__(self)
		self.stdout0 = None
		self.stderr0 = None
		self.success_count = 0
		self.failure_count = 0
		self.error_count = 0
		self.verbosity = verbosity
		self.outputBuffer = None
		# 将责任人信息挂到具体的TestMethod Result上
		self.responsible_person = responsible_person

		# result is a list of result in 4 tuple
		# (
		#   result code (0: success; 1: fail; 2: error),
		#   TestCase object,
		#   Test output (byte string),
		#   stack trace,
		# )
		self.result = []
		self.subtestlist = []
		# 记录运行时长
		self._start_clock = 0
		self._end_clock = 0

	def startTest(self, test):
		# 1 step
		# 登记已运行案例个数
		xml = ParseXml(filepath=os.path.join(Config.project_dir, "automation.data"))
		executed_count = int(xml.get_value_from_xpath(xpath="./BuildData/ExecProgress", attribute="executed"))
		xml.set_value_from_xpath(xpath="./BuildData/ExecProgress", attribute="executed", value=str(executed_count + 1))
		pg.SCREENSHOTS_NAME = []
		self._start_clock = time.perf_counter()
		TestResult.startTest(self, test)
		# just one buffer for both stdout and stderr
		self.outputBuffer = io.StringIO()
		stdout_redirector.fp = self.outputBuffer
		stderr_redirector.fp = self.outputBuffer
		self.stdout0 = sys.stdout
		self.stderr0 = sys.stderr
		sys.stdout = stdout_redirector
		sys.stderr = stderr_redirector

	def complete_output(self):
		"""
		Disconnect output redirection and return buffer.
		Safe to call multiple times.
		"""
		if self.stdout0:
			sys.stdout = self.stdout0
			sys.stderr = self.stderr0
			self.stdout0 = None
			self.stderr0 = None
		return self.outputBuffer.getvalue()

	def stopTest(self, test):
		# Usually one of addSuccess, addError or addFailure would have been called.
		# But there are some path in unittest that would bypass this.
		# We must disconnect stdout in stopTest(), which is guaranteed to be called.
		self.complete_output()

	def addSuccess(self, test):
		if test not in self.subtestlist:
			self.success_count += 1
			TestResult.addSuccess(self, test)
			output = self.complete_output()
			self._end_clock = time.perf_counter()
			elapsed_time = self._end_clock - self._start_clock
			self.result.append((0, test, output, "", pg.SCREENSHOTS_NAME, elapsed_time, self.responsible_person))
			if self.verbosity > 1:
				sys.stderr.write("ok ")
				sys.stderr.write(str(test))
				sys.stderr.write("\n")
			else:
				sys.stderr.write(".")

	def addError(self, test, err):
		self.error_count += 1
		# zz: 2019年7月25日 案例执行失败或错误时自动截图保存在 report\tempimg\目录中
		# screenshot(self._get_screenshot_path(test=test))
		TestResult.addError(self, test, err)
		_, _exc_str = self.errors[-1]
		output = self.complete_output()
		self._end_clock = time.perf_counter()
		elapsed_time = self._end_clock - self._start_clock
		self.result.append((2, test, output, _exc_str, pg.SCREENSHOTS_NAME, elapsed_time, self.responsible_person))
		if self.verbosity > 1:
			sys.stderr.write("E  ")
			sys.stderr.write(str(test))
			sys.stderr.write("\n")
		else:
			sys.stderr.write("E")

	def addFailure(self, test, err):
		self.failure_count += 1
		# zz: 2019年7月25日 案例执行失败或错误时自动截图保存在 report\tempimg\目录中
		# screenshot(self._get_screenshot_path(test=test))
		TestResult.addFailure(self, test, err)
		_, _exc_str = self.failures[-1]
		output = self.complete_output()
		self._end_clock = time.perf_counter()
		elapsed_time = self._end_clock - self._start_clock
		self.result.append((1, test, output, _exc_str, pg.SCREENSHOTS_NAME, elapsed_time, self.responsible_person))
		if self.verbosity > 1:
			sys.stderr.write("F  ")
			sys.stderr.write(str(test))
			sys.stderr.write("\n")
		else:
			sys.stderr.write("F")

	def addSubTest(self, test, subtest, err):
		if err is not None:
			if getattr(self, "failfast", False):
				self.stop()
			if issubclass(err[0], test.failureException):
				self.failure_count += 1
				errors = self.failures
				errors.append((subtest, self._exc_info_to_string(err, subtest)))
				output = self.complete_output()
				self.result.append(
					(
						1,
						test,
						output + "\nSubTestCase Failed:\n" + str(subtest),
						self._exc_info_to_string(err, subtest),
					)
				)
				if self.verbosity > 1:
					sys.stderr.write("F  ")
					sys.stderr.write(str(subtest))
					sys.stderr.write("\n")
				else:
					sys.stderr.write("F")
			else:
				self.error_count += 1
				errors = self.errors
				errors.append((subtest, self._exc_info_to_string(err, subtest)))
				output = self.complete_output()
				self.result.append(
					(
						2,
						test,
						output + "\nSubTestCase Error:\n" + str(subtest),
						self._exc_info_to_string(err, subtest),
					)
				)
				if self.verbosity > 1:
					sys.stderr.write("E  ")
					sys.stderr.write(str(subtest))
					sys.stderr.write("\n")
				else:
					sys.stderr.write("E")
			self._mirrorOutput = True
		else:
			self.subtestlist.append(subtest)
			self.subtestlist.append(test)
			self.success_count += 1
			output = self.complete_output()
			self.result.append(
				(0, test, output + "\nSubTestCase Pass:\n" + str(subtest), "")
			)
			if self.verbosity > 1:
				sys.stderr.write("ok ")
				sys.stderr.write(str(subtest))
				sys.stderr.write("\n")
			else:
				sys.stderr.write(".")

	def _get_screenshot_path(self, test):
		"""
			返回test的信息，供截图保存时命名
			文件名会以"[FE]" 标识，表示是由于失败(Failure)或错误(Error)自动生成的截图
		:param test: TestCase
		:return: '.\Project_Selenium\\report\\tempimg\\TestCases.test_案例_1.案例1_搜索测试.test_1_百度搜索测试_[FE].png'
		:rtype: str
		"""
		random_int = random.randint(10000000, 99999999)  # 随机数
		screenshot_name = (test.__module__
						   + "."
						   + test.__class__.__name__
						   + "."
						   + test._testMethodName
						   + "_"
						   + str(random_int)
						   + "_[FE]"
							 ".png")
		path = os.path.join(
			Config.project_dir,
			"report",
			"tempimg",
			screenshot_name)
		pg.SCREENSHOTS_NAME.append(screenshot_name)
		return path


class _NewTestResult:
	def __init__(self,
				 success_count,
				 failure_count,
				 error_count,
				 result,
				 stop_time,
				 start_time,
				 title,
				 description,
				 file_name,
				 sheet_name,
				 responsible_person_of_failure_or_error_result):
		self.success_count = success_count  # result.success_count = testResult.success_count
		self.failure_count = failure_count  # result.failure_count = testResult.failure_count
		self.error_count = error_count  # result.error_count = testResult.error_count
		self.result = result  # result.result = testResult.result
		self.stop_time = stop_time  # htmlTestRunner.stop_time = testResult.stop_time
		self.start_time = start_time  # htmlTestRunner.start_time = testResult.start_time
		self.title = title  # htmlTestRunner.title = testResult.title
		self.description = description  # htmlTestRunner.description = testResult.description
		self.file_name = file_name  # Excel name
		self.sheet_name = sheet_name  # sheet_name
		# 场景中执行失败或错误时的责任人
		self.responsible_person_of_failure_or_error_result = responsible_person_of_failure_or_error_result


class HTMLTestRunner(TemplateMixin):
	# 2018年9月16日 zz: 在构造函数中增加report参数，类型为bool，True表示需要输出报告，False则不输出。
	def __init__(self,
				 stream=sys.stdout,
				 verbosity: int = 1,
				 title: str = None,
				 description: str = None,
				 report: bool = True):
		self.stream = stream
		self.verbosity = verbosity
		self.report = report  # 是否输出报告
		if title is None:
			self.title = self.DEFAULT_TITLE
		else:
			self.title = title
		if description is None:
			self.description = self.DEFAULT_DESCRIPTION
		else:
			self.description = description
		project_dir = os.path.dirname(
			os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
		)  # 项目目录
		if self.report:
			self.report_name = os.path.basename(self.stream.name).split(".")[0]  # 获取报告文件名
			self.report_dir = os.path.join(project_dir, "Report", self.report_name)  # 报告目录
		# self.report_img_dir = os.path.join(self.report_dir, "img")  # 报告目录中的img目录
		# self.temp_img_dir = os.path.join(project_dir, "Report", "tempimg")  # project/Report/tempimg
		self.stop_time = None
		self.start_time = datetime.datetime.now()
		self.responsible_person_of_failure_or_error_result = []

	def run(self, test):
		"""
			Run the given test case or test suite.

			通过run.py执行时，这里传入test的实参为一个suite。这个suite是一个sheet页中所有用例产生的
		"""
		if hasattr(test, 'responsible_person'):
			responsible_person = test.responsible_person
		else:
			responsible_person = ()
		result = _TestResult(self.verbosity, responsible_person=responsible_person)
		# test(result)  调用了Class BaseTestSuite.run(self, result) 方法，并返回result
		result = test(result)  # suite的result 即一个sheet执行的result
		self.stop_time = datetime.datetime.now()
		if self.report:
			self.generate_report(test, result)
		print("\nTime Elapsed: %s" % (self.stop_time - self.start_time), file=sys.stderr)
		# return result # 2018年9月11日 将返回对象改成二次封装的newtestresult对象
		if hasattr(test, 'sheet_name'):
			sheet_name = test.sheet_name
		else:
			sheet_name = ''
		if hasattr(test, 'file_name'):
			file_name = test.file_name
		else:
			file_name = ''
		# 筛选出错误和失败案例对应的责任人
		self.responsible_person_of_failure_or_error_result = self._get_responsible_person_of_failure_or_error_result(
			result)
		new_test_result = _NewTestResult(
			success_count=result.success_count,
			failure_count=result.failure_count,
			error_count=result.error_count,
			result=result.result,  # list 里面是每个测试类(TestCase)中每个方法(TestMethod)的result集合
			stop_time=self.stop_time,
			start_time=self.start_time,
			title=self.title,
			description=self.description,
			sheet_name=sheet_name,
			file_name=file_name,
			responsible_person_of_failure_or_error_result=self.responsible_person_of_failure_or_error_result
		)

		return new_test_result

	def sort_result(self, result_list):
		"""
		不论unittest运行的测试套件(suite)是以什么方式组织运行
		这里我们通过测试案例所属的类将其组织一起返回。
		return:
			[
				(class_1, [(0/1/2, TestCase_1, OutPut, ''/_exc_str), (0/1/2, TestCase_2, OutPut, ''/_exc_str)], ...), \
				(class_2, [(0/1/2, TestCase_3, OutPut, ''/_exc_str), ...), \
				...
			]
		"""
		# unittest does not seems to run in any particular order.
		# Here at least we want to group them together by class.
		r_map = {}
		classes = []
		for n, t, o, e, screenshots_id, elapsed_time, responsible_person in result_list:
			# n(结果): 0-成功 1-失败 2-错误；
			# t(测试案例类(TestCase),包含了此次执行的场景(_testMethodName))
			# o(打印内容)
			# e(错误堆栈信息)
			# screenshots_id(该案例(testMethod)中的截图id)
			# elapsed_time(该案例(testMethod)运行时长)
			# responsible_person(该案例(testMethod所属的Suite对应的责任人))
			cls = t.__class__
			if cls not in r_map:
				r_map[cls] = []
				classes.append(cls)
			r_map[cls].append((n, t, o, e, screenshots_id, elapsed_time, responsible_person))
		r = [(cls, r_map[cls]) for cls in classes]
		return r

	def get_report_attributes(self, result):
		"""
		Return report attributes as a list of (name, value).
		Override this to add custom attributes.
		"""
		start_time = str(self.start_time)[:19]
		duration = str(self.stop_time - self.start_time)
		status = []
		if result.success_count:
			status.append(u"通过 %s" % result.success_count)
		if result.failure_count:
			status.append(u"失败 %s" % result.failure_count)
		if result.error_count:
			status.append(u"错误 %s" % result.error_count)
		if status:
			status = " ".join(status)
		else:
			status = "none"
		return [(u"开始时间", start_time), (u"运行时长", duration), (u"状态", status), ("环境信息", result.environment)]

	def generate_report(self, test, result):
		# 将./Report/tempimg目录下的截图移动到具体的报告./img目录中
		# shutil.copytree(self.temp_img_dir, self.report_img_dir)
		report_attrs = self.get_report_attributes(result)
		generator = "HTMLTestRunner %s" % __version__
		stylesheet = self._generate_stylesheet()
		heading = self._generate_heading(report_attrs)
		report = self._generate_report(result)
		ending = self._generate_ending()
		chart = self._generate_chart(result)
		output = self.HTML_TMPL % dict(
			title=saxutils.escape(self.title),
			generator=generator,
			stylesheet=stylesheet,
			heading=heading,
			report=report,
			ending=ending,
			chart_script=chart,
		)
		self.stream.write(output.encode("utf8"))

	def _generate_stylesheet(self):
		return self.STYLESHEET_TMPL

	def _generate_heading(self, report_attrs):
		a_lines = []
		for name, value in report_attrs:
			line = self.HEADING_ATTRIBUTE_TMPL % dict(
				name=saxutils.escape(name), value=saxutils.escape(value)
			)
			a_lines.append(line)
		heading = self.HEADING_TMPL % dict(
			title=saxutils.escape(self.title),
			parameters="".join(a_lines),
			description=saxutils.escape(self.description),
		)
		return heading

	def _generate_report(self, result):
		rows = []
		# result.test_method_results: [(成功/失败/错误, TestCase, 打印内容, 成功时为空/失败打印断言内容/错误打印错误信息)]
		sorted_result = self.sort_result(result.test_method_results)
		total_elapsed_time = 0
		for cid, (cls, cls_results) in enumerate(sorted_result):
			# subtotal for a class
			np = nf = ne = 0
			# for n, t, o, e, screenshots in cls_results:
			class_elapsed_time = 0
			for n, *_, elapsed_time, responsible_person in cls_results:
				class_elapsed_time += elapsed_time
				if n == 0:
					np += 1
				elif n == 1:
					nf += 1
				else:
					ne += 1

			# format class description
			if cls.__module__ == "__main__":
				name = cls.__name__
			else:
				name = "%s.%s" % (cls.__module__, cls.__name__)
			doc = cls.__doc__ and cls.__doc__.split("\n")[0] or ""
			desc = doc and "%s: %s" % (name, doc) or name

			# 表格中最外层tr显示内容 “test.test_案例_1.案例1_搜索测试 	1 	1 	0 	0 	详情”
			row = self.REPORT_CLASS_TMPL % dict(
				style=ne > 0 and "errorClass" or nf > 0 and "failClass" or "passClass",
				desc=desc,
				count=np + nf + ne,
				Pass=np,
				fail=nf,
				error=ne,
				cid="c%s" % (cid + 1),
				elapsed_time=class_elapsed_time,
				responsible_person=",".join(responsible_person),
			)
			rows.append(row)
			# 汇总一个类中所有测试案例的执行结果
			for tid, (n, t, o, e, screenshots, elapsed_time, *_) in enumerate(cls_results):
				self._generate_report_test(rows, cid, tid, n, t, o, e, screenshots, elapsed_time)
			total_elapsed_time += class_elapsed_time
		# test_list 表格中测试案例运行情况
		# count\Pass\fail\error 为表格最后一行“总计”
		report = self.REPORT_TMPL % dict(
			test_list="".join(rows),
			count=str(result.success_count + result.failure_count + result.error_count),
			Pass=str(result.success_count),
			fail=str(result.failure_count),
			error=str(result.error_count),
			elapsed_time=total_elapsed_time,
		)
		return report

	def _generate_chart(self, result):
		chart = self.ECHARTS_SCRIPT % dict(
			Pass=str(result.success_count),
			fail=str(result.failure_count),
			error=str(result.error_count),
		)
		return chart

	def _generate_report_test(self, rows, cid, tid, n, t, o, e, screenshots, elapsed_time):
		# e.g. 'pt1.1', 'ft1.1', etc
		has_output = bool(o or e)
		tid = (n == 0 and "p" or "f") + "t%s.%s" % (cid + 1, tid + 1)
		name = t.id().split(".")[-1]
		# print("_generate_report_test name:", name)
		doc = t.shortDescription() or ""
		desc = doc and ("%s: %s" % (name, doc)) or name
		tmpl = (
				has_output
				and self.REPORT_TEST_WITH_OUTPUT_TMPL
				or self.REPORT_TEST_NO_OUTPUT_TMPL
		)

		script = self.REPORT_TEST_OUTPUT_TMPL % dict(
			id=tid, output=saxutils.escape(o + e)
		)
		# 汇总一个测试案例执行结果
		row = tmpl % dict(
			tid=tid,
			Class=(n == 0 and "hiddenRow" or "none"),
			style=(n == 2 and "errorCase" or (n == 1 and "failCase" or "none")),
			desc=desc,
			script=script,
			status=self.STATUS[n],
			img=self._generate_img(screenshots=screenshots),
			elapsed_time=elapsed_time,
		)
		rows.append(row)
		if not has_output:
			return

	def _generate_ending(self):
		return self.ENDING_TMPL

	def _generate_img(self, screenshots: list):
		"""
			生成截图DOM <img />元素信息
		:param screenshots: 截图文件名
		:return:
			<td>
				<img />
			</td>
		"""
		# 在reportimgdir目录中查找有无当前案例的截图
		fe_screenshot_path_list = []
		running_screenshot_path_list = []
		for screenshot_name in screenshots:
			# zz 2018年7月28日 绝对路径改为相对路径;
			if "[FE]" in screenshot_name:
				fe_screenshot_path_list.append(".\\img\\" + screenshot_name)
			else:
				running_screenshot_path_list.append(".\\img\\" + screenshot_name)
		running_img_dmo_list = [
			"""<img src="%s" width="100px" height="100px" onClick='changeimg(this)'/>"""
			% running_screenshot_path
			for running_screenshot_path in running_screenshot_path_list
		]
		fe_img_dom_list = [
			"""<img src="%s" width="100px" height="100px" onClick='changeimg(this)'/>"""
			% fe_screenshot_path
			for fe_screenshot_path in fe_screenshot_path_list
		]
		running_img_td = self.IMG_TMPL % dict(img="".join(running_img_dmo_list))
		fe_img_td = self.IMG_TMPL % dict(img="".join(fe_img_dom_list))
		img = running_img_td + "\n" + fe_img_td
		return img

	# def __getImageSize(self, path: str) -> tuple:
	#     """
	#     说明：
	#         获取图片的像素
	#     :param path: 图片文件路径
	#     :return: (长px, 宽px) tuple
	#     """
	#     return Image.open(path).size

	@staticmethod
	def _get_responsible_person_of_failure_or_error_result(result) -> Tuple:
		"""
		筛选出错误和失败案例对应的责任人
		:param result: class _TestResult
		:return: class tuple
		"""
		if result.error_count + result.failure_count >= 1:
			return result.responsible_person if result.responsible_person is not None else tuple()
		else:
			return tuple()


##############################################################################
# Facilities for running tests from the command line
##############################################################################

# Note: Reuse unittest.TestProgram to launch test. In the future we may
# build our own launcher to support more specific command line
# parameters like test title, CSS, etc.
class TestProgram(unittest.TestProgram):
	"""
	A variation of the unittest.TestProgram. Please refer to the base
	class for command line parameters.
	"""

	def runTests(self):
		# Pick HTMLTestRunner as the default test runner.
		# base class's testRunner parameter is not useful because it means
		# we have to instantiate HTMLTestRunner before we know self.verbosity.
		if self.testRunner is None:
			self.testRunner = HTMLTestRunner(verbosity=self.verbosity)
		unittest.TestProgram.runTests(self)


main = TestProgram

##############################################################################s
# Executing this module from the command line
##############################################################################

# if __name__ == "__main__":
#     main(module=None)
